"This is a tools container module"

from hashlib import md5
from copy import deepcopy
import numpy as np

def pacecounter(seq, elems):
	"This function returns dictionary of paces for (dicts, dicts order) tuple and elems list"
	#beginning of function suite
	levlen=len(seq)
	result={}
	for x in range(levlen):
		result[seq[x]]=[1,1]
		for y in range(levlen-1, x, -1):
			result[seq[x]][0]*=len(elems[seq[y]])
		for z in range(0,x):
			result[seq[x]][1]*=len(elems[seq[z]])
	result['len']=len(elems[seq[0]])*result[seq[0]][0]*result[seq[0]][1]
	#print(result['len'])
	#end of function suite
	return result

def headfiller(seq, elems, paces):
	"This function returns dictionary of elements list for head in seq with the given elems and pace dictionaries"
	#beginning of function suite
	result={}
	for head in seq:
		result[head]=[]
		for elem in elems[head]:
			result[head]+=[elem]*paces[head][0]
		result[head]*=paces[head][1]
	#end of function suite
	return result



def uniqueitems(generator, length):
	"This function returns list of unique elements lists from lists generator"
	#beginning of function suite
	#Получение списка пустых списков умножением пустого списка на числов повторений приводит к тому, что добавление элемента к одному из них добавляет этот элемент к каждому из созданных таким образом пустых списков, поэтому создаем список пустых списков в цикле прибалением к пустому списку пустых подсписков
	result=[]
	for x in range(length):
		result+=[[]]
	for item in generator:
		for x in range(length):
			if item[x] not in result[x]:
				result[x]+=[item[x]]
	# end of function suite
	return result


def cond_check(conditions, dims, elems):
	"This function checks if conditions are valid against dims, returns [Boolean, [Error string]]"
	#beginning of function suite
	result=[True,[]]
	for con in conditions:
		if con in dims:
			if type(conditions[con])==slice:
				if conditions[con].start==conditions[con].stop:
					result[1]+=["Dim: "+str(con)+". Slice must not be empty"]
					result[0]=False
				else:
					for item in range(conditions[con].start, conditions[con].stop):
						if item not in elems[con]:
							result[1]+=["Dim: "+str(con)+". Slice element: "+str(item)+" not in the dimension"]
							result[0]=False
			elif type(conditions[con])==list:
				if conditions[con]==[]:
					result[1]+=["Dim: "+str(con)+". List must not be empty"]
					result[0]=False
				else:
					for item in conditions[con]:
						if item not in elems[con]:
							result[1]+=["Dim: "+str(con)+". List element: "+str(item)+" not in the dimension"]
							result[0]=False
			else:
				result[1]+=["Dim: "+str(con)+". Condition must be either list or slice"]
				result[0]=False
		else:
			result[1]+=["Dim: "+str(con)+". Dimension not in the dimensions list"]
			result[0]=False
	#end of function suite
	return result

def filled(dims_nda, elems, data, null):
	"This function returns dictionary of filled elements from elements dictionary, dims_nda list, numpy array data and null value"
	#beginning of function suite
	result={}
	result=deepcopy(elems)
	#print(list(result))
#	print(np.shape(data))
#	print(dims_nda)
#	print(np.unique(np.where(data!=null)))
	for dim in dims_nda:
		x=dims_nda.index(dim)
		
		result[dim]=[]
		#print(np.shape(data))
		#print(dims_nda)
		#print(np.unique(np.where(data!=null)[x]))
		#print(len(elems[dim]))
#		print(elems[dim][:15])
		
#		print(np.unique(np.where(data!=null)[x])[:15])
		for y in np.unique(np.where(data!=null)[x]):
#			print(y)
			result[dim]+=[elems[dim][y]]
	#end of function suite
	return result

def slicer(dims_nda, new_con, elems, data, reduce=True):
	"This function returns new dims_nda, elems, data according to provided dims_nda, new_con, elems, data, reduce. If reduce=True then number of nda dimensions is reduced by single index else is not"
	#beginning of function suite
	#Формируется пустой словарь результатов
	result={}
	#Создается пустой список индексов многомерного массива для выборки
	ind=[]
	#Для каждого измерения
	for dim in dims_nda:
		#print(dims_nda)
		#Если измерение есть в условиях выборки
		if dim in new_con:
			#Создается новый список элементов данного измерения
			newlist=[]
			#Создается список индексов данного измерения
			subindex=[]
			#Если условие для данного измерения является слайсом
			if type(new_con[dim]) == slice:
				start=elems[dim].index(new_con[dim].start)
				stop=elems[dim].index(new_con[dim].stop-1)+1
				subindex=slice(start,stop)
				for x in range(start, stop):
					newlist+=[elems[dim][x]]
				if reduce and start+1==stop:
					subindex=start
			#Если условие для данного измерения является списком:
			else:
				for item in new_con[dim]:
					subindex+=[elems[dim].index(item)]
					newlist+=[item]
				if not reduce and len(subindex)==1:
					subindex=slice(subindex[0],subindex[0]+1)
				elif reduce and len(subindex)==1:
					subindex=subindex[0]
			#В индекс помещается список индексов для данного измерения
			ind+=[subindex]
			#Данному измерению присваивается новый список элементов
			elems[dim] = newlist
		#Если измерения нет в условиях выборки
		else:
			#print(elems[dim])
			#К индексу прибавляется slice от нуля до длины измерения
			if reduce and len(elems[dim])==1:
				ind+=[0]
			else:
				ind+=[slice(0,len(elems[dim]))]
			#print(ind)
	#Если внутри цикла for удалить dim, количество элементов в котором меньше двух, то обход сбивается и сразу переходит на элемент через один после удаленного, поэтому повторяем обход списка dims_nda
	#Создается копия списка измерений нда, потому что при удалении элемента из списка обхода, игнорируется следующий элемент списка
	temp=deepcopy(dims_nda)
	#Для каждого измерения в dims_nda
	for dim in temp:
		#Если в измерении nda количество элементов меньше двух
		#print(elems[dim][:15])
		if reduce and len(elems[dim])<2:
			dims_nda.remove(dim)
	#print(dims_nda)
	
	#Формируется новый nda выборкой из старого
	if ind:
		#print(tuple(ind))
		data=data[tuple(ind)]
	
	result["dims_nda"]=dims_nda
	result["elems"]=elems
	result["data"]=data

	#end of function suite
	return result

def subdata(new_con, conditions, dims, dims_nda, elems, null, data, reduce=True):
	"This function returns new conditions, dims_nda, elems, elems_filled, shape, shape_nda, width_nda, data according to new_con"
	#beginning of function suite
	#Выполняется проверка действительности новых условий относительно измерений и элементов
	check=cond_check(new_con, dims, elems)
	#Если проверка пройдена успешно:
	if check[0]:
		#Формируется словарь результатов
		new=slicer(dims_nda, new_con, elems, data, reduce)
		#В словарь результатов добавляется словарь непустых элементов 'elems_filled'
		new["elems_filled"]=filled(new["dims_nda"], new["elems"], new["data"], null)
		#Обновляются условия 
		conditions.update(new_con)
		#В словарь результатов добавляются обновленные условия: /Прямое приравнивание не работает/
		new["conditions"]=conditions
		#В словпь результатов добавляются другие параметры объекта
		new["shape"]=[]
		for dim in dims:
			new["shape"]+=[len(new["elems"][dim])]
		new["shape_nda"]= np.shape(new["data"])
		new["width_nda"]= len(new["shape_nda"])
	#Если условия не действительны относительно измерений и элементов
	else:
		#Вызывается исключение с текстом ошибки
		raise Exception(';\n'.join(check[1]))
	#end of function suite
	#Функция возвращает словарь результатов
	return new

def nda_emp_str(shape, length):
	"Функция возвращает пустой многомерный массив текстового типа"
	#beginning of function suite
	#Создается прототип многомерного массива соответствующего размера и с соответствующей длиной каждого элемента
	proto='t'*length
	for x in range(len(shape)-1,-1,-1):
		proto=list([proto]*shape[x])
	#Из прототипа создается многомерный массив
	result=np.array(proto)
	#Всем непустым элементам nda присваивается строка нулевой длины
	result[np.where(result)]=''
	#end of function suite
	return result

def meter(typevalue, coord, valuecoord, dimensions):
	"This function returns metrics for cube cells generator"
	#beginning of function suite
	metrics={}
	metrics["width"]=0
	metrics["width_nda"]=0
	metrics["shape"]=[]
	metrics["shape_nda"]=[]
	metrics["dims"]=[]
	metrics["dims_nda"]=[]
	metrics["elems"]={}
	metrics["elems_filled"]={}
	metrics["length"]=0
	metrics["type"]=1
	metrics["null"]=None
	metrics["data"]=None
	
	metrics["width"]=len(dimensions)
	coordinates=uniqueitems(coord, metrics["width"])
	
	# Определеяется type, length
	for item in typevalue:
		if metrics["type"]==1 and item[0]==2:
			metrics["type"]=2
		if metrics["length"]<len(str(item[1])):
			metrics["length"]=len(str(item[1]))
	
	# Определяется dims, elems, shape
	for dimension in dimensions:
		x=dimensions.index(dimension)
		metrics["dims"]+=[dimension]
		metrics["elems"][dimension]=coordinates[x]
		metrics["shape"]+=[len(metrics["elems"][dimension])]
	
	if metrics["type"]==2:
		#Создается пустой текстовый nda размером shape
		metrics["data"]=nda_emp_str(metrics["shape"], metrics["length"])
		metrics["null"]=''
		#Иначе
	else:
		#Создается нулевой nda размером shape
		metrics["data"]=np.zeros(metrics["shape"])
		metrics["null"]=0
	
	for row in valuecoord:
		#Создается пустой список nda индексов строки:
		ind=[]
		#Для каждой позиции координаты
		for dimension in dimensions:
			x=dimensions.index(dimension)
			#В список nda индексов строки добавляется индекс позиции координаты в списке элементов соответствующего измерения nda:
			ind+=[metrics["elems"][dimension].index(row[1][x])]
		value = row[0] or metrics["null"]
		#В элемент nda с соответствующим списком индексов записывается выгруженное значение
		metrics["data"][tuple(ind)]=value
	
	metrics["elems_filled"]=filled(metrics["dims"], metrics["elems"], metrics["data"], metrics["null"])
	
	metrics["dims_nda"]=deepcopy(metrics["dims"])
	metrics["shape_nda"]=np.shape(metrics["data"])
	metrics["width_nda"]=len(metrics["shape_nda"])

	return metrics
	#end of function suite

def arealize(conditions, dimensions):
	"This function returns cube area from conditions dictionary"
	#beginning of function suite
	result=[]
	for dim in dimensions:
		if dim in conditions:
			if type(conditions[dim])==list:
				result+=[':'.join(strlist(conditions[dim]))]
			elif type(conditions[dim])==slice:
				temp=[]
				for x in range(conditions[dim].start, conditions[dim].stop):
					temp+=[x]
				result+=[':'.join(strlist(temp))]
			else:
				raise Exception("Dim: "+str(dim)+". Condition must be either list or slice")
		else:
			result+=['*']
	result=','.join(result)
	#end of function suite
	return result

def strlist(alist):
	"This function returns list of strs from a list of ints"
	#beginning of function suite
	#end of function suite
	return list(np.array(alist).astype(str))

def sublist(alist, indices=None):
	"This function returns sublist from a list"
	#beginning of function suite
	if indices==None:
		result=alist
	elif type(indices)==list:
		result=[]
		for item in indices:
			result+=[alist[item]]
	else:
		raise Exception("Indices must be list or None")
	#end of function suite
	return result

def normalize(undef):
	"This function returns python object from csv respond"
	#beginning of function suite
	try:
		result=int(undef)
	except:
		try:
			result=float(undef)
		except:
			if '"' in undef:
				result=undef.replace('"','')
			elif ',' in undef:
				result=[]
				for item in undef.split(','):
					if ':' in item:
						temp=[]
						for subitem in item.split(':'):
							temp+=[normalize(subitem)]
						result+=[temp]
					else: 
						result+=[normalize(item)]
			else: result=undef
	#end of function suite
	return result

def tobytes(string):
	'''This function converts string to bytes.
	USAGE:
	tobytes(string) returns bytes'''
	#beginning of function suite
	
	if type(string) is str:
		result=string.encode('utf-8')
		log='converted successfully'
	else:
		result=None
		log='argument is not of type str'
	#end of function suite
	return [result,log]

def tomd5(string):
	'''This function returns md5 of a string.
	USAGE:
	tomd5(string) returns md5'''
	#beginning of function suite
	#end of function suite
	return md5(tobytes(string)[0]).hexdigest()